﻿namespace ePubReader
{
    using Ionic.Zip;
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Text;
    using System.Text.RegularExpressions;
    using System.Xml.Linq;

    public class Epub : System.IDisposable
    {
        #region fields

        Regex bodyRegex;

        Regex titleRegex;

        Regex styleRegex;

        Regex cssRegex;

        Regex imgRegex;

        Dictionary<string, byte[]> resources;

        string contentFolder;

        ZipFile epubFile;

        #endregion

        public Epub(Stream fileStream)
        {
            bodyRegex = new Regex(@"<body[^>]*>(?<body>.+)</body>", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline);
            styleRegex = new Regex(@"<style[^>]*>(?<style>.+)</style>", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline);
            titleRegex = new Regex(@"<title[^>]*>(?<title>.+)</title>", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline);
            cssRegex = new Regex(@"<link\s+[^>]*?(href\s*=\s*(""|')(?<href>[^""']+)(""|')[^>]*?|type\s*=\s*(""|')text/css(""|')[^>]*?){2}[^>]*?/>", RegexOptions.Compiled | RegexOptions.IgnoreCase);
            imgRegex = new Regex(@"(?<prefix><\w+[^>]*?src\s*=\s*(""|'))(?<src>[^""']+)(?<suffix>(""|')[^>]*>)", RegexOptions.Compiled | RegexOptions.IgnoreCase);

            resources = new Dictionary<string, byte[]>();
            epubFile = ZipFile.Read(fileStream);

            Initialize();
        }

        #region Properties

        public Package Package { get; private set; }

        public Nav[] TOC { get; private set; }

        public string Title { get { return Package.Metadata.Title; } }

        public string Identifier { get { return Package.Metadata.Identifier; } }

        public string Author
        {
            get
            {
                if (Package.Metadata.Creators != null)
                {
                    return string.Join(", ", Package.Metadata.Creators.Select(x => x.Name));
                }

                return string.Empty;
            }
        }

        public string Language { get { return Package.Metadata.Language; } }

        public string Published
        {
            get
            {
                if (Package.Metadata.Dates != null)
                {
                    if (Package.Metadata.Dates.Length == 1)
                    {
                        return Package.Metadata.Dates[0].DateTime;
                    }
                    else if (Package.Metadata.Dates.Length > 1)
                    {
                        if (Package.Metadata.Dates.Any(x => x.Name == "modification"))
                        {
                            return Package.Metadata.Dates.FirstOrDefault(x => x.Name == "modification").DateTime;
                        }
                        else
                        {
                            return Package.Metadata.Dates[0].DateTime;
                        }
                    }
                }

                return DateTime.Now.ToShortDateString();
            }
        }

        public byte[] Cover
        {
            get
            {
                string coverImage = "cover-image";

                // epub 2.x
                if ((int)Package.Version == 2)
                {
                    coverImage = Package.Metadata.CoverContent;
                    if (string.IsNullOrEmpty(coverImage))
                    {
                        coverImage = "cover-image";
                    }

                    if (Package.Manifest.Any(x => coverImage.Equals(x.Id, StringComparison.OrdinalIgnoreCase)))
                    {
                        Item coverItem = Package.Manifest.First(x => coverImage.Equals(x.Id, StringComparison.OrdinalIgnoreCase));
                        return GetBinaryResources(string.Format("{0}/{1}", contentFolder, coverItem.Href));
                    }
                }
                else if (Package.Manifest.Any(x => x.Properties.Contains(coverImage)))
                {
                    Item coverItem = Package.Manifest.First(x => x.Properties.Contains(coverImage));
                    return GetBinaryResources(string.Format("{0}/{1}", contentFolder, coverItem.Href));
                }

                return new byte[0];
            }
        }

        public ZipFile Zip { get { return epubFile; } }

        public string ContentFolder { get { return contentFolder; } }

        #endregion

        #region public methods

        public Content GetContent(string source)
        {
            string title = string.Empty;
            string body = string.Empty;
            List<string> styles = new List<string>();

            if (Package.Manifest.Any(x => x.Href.EndsWith(source)))
            {
                Item fileItem = Package.Manifest.First(x => x.Href.EndsWith(source));
                source = string.Format("{0}/{1}", contentFolder, fileItem.Href);

                ZipEntry fileEntity = epubFile.Entries.FirstOrDefault(x => source.Equals(x.FileName, StringComparison.OrdinalIgnoreCase));
                if (fileEntity != null)
                {
                    string html = string.Empty;
                    using (MemoryStream ms = new MemoryStream())
                    using (StreamReader sr = new StreamReader(ms))
                    {
                        fileEntity.Extract(ms);
                        ms.Position = 0;

                        html = sr.ReadToEnd();
                    }

                    // handle css.
                    MatchCollection cssMatches = cssRegex.Matches(html);
                    foreach (Match m in cssMatches)
                    {
                        styles.Add(CssEvaluator(m));
                    }

                    // handle style
                    MatchCollection styleMathes = styleRegex.Matches(html);
                    foreach (Match m in styleMathes)
                    {
                        styles.Add(m.Value);
                    }

                    // handle body.
                    Match bodyMatch = bodyRegex.Match(html);
                    if (bodyMatch.Success)
                    {
                        body = imgRegex.Replace(bodyMatch.Groups["body"].Value, ImagesEvaluator);
                    }

                    // handle title
                    Match titleMatcn = titleRegex.Match(html);
                    if (titleMatcn.Success)
                    {
                        title = titleMatcn.Groups["title"].Value;
                    }
                }
            }

            return new Content()
            {
                Title = title,
                Body = body,
                Styles = styles.ToArray()
            };
        }

        /// <summary>
        /// Extract epub files to iso storage.
        /// </summary>
        /// <param name="path">ISO storage path, e.g common/abc</param>
        public void Extract(string path)
        {
            this.epubFile.ExtractAll(path);
        }

        #endregion

        #region private methods

        private void Initialize()
        {
            string opfPath = GetOpfFilePath(epubFile);

            // build contentPath
            string[] pathTokens = opfPath.Split(new[] { '/' });
            if (pathTokens.Length > 1)
            {
                contentFolder = string.Join("/", pathTokens.Take(pathTokens.Length - 1));
            }
            else if (pathTokens.Length == 1)
            {
                contentFolder = "/";
            }
            else
            {
                throw new Exception("Invalid epub file.");
            }

            // build Package
            ZipEntry zipEntry = epubFile.Entries.FirstOrDefault(e => opfPath.Equals(e.FileName, StringComparison.OrdinalIgnoreCase));
            if (zipEntry == null)
            {
                throw new Exception("Invalid epub file.");
            }

            using (MemoryStream memoryStream = new MemoryStream())
            using (StreamReader sr = new StreamReader(memoryStream))
            {
                zipEntry.Extract(memoryStream);
                memoryStream.Position = 0;

                Package = PackageReader.Read(sr.ReadToEnd());
            }

            // build nav.
            switch ((int)Package.Version)
            {
                case 3:
                    TOC = GetEpub3Navs(Package, epubFile, contentFolder);
                    break;

                default:
                    TOC = GetEpubNavs(Package, epubFile, contentFolder);
                    break;
            }
        }

        private static string GetOpfFilePath(ZipFile zipFile)
        {
            string fullpath = string.Empty;
            ZipEntry zipEntry = zipFile.Entries.FirstOrDefault(e => "meta-inf/container.xml".Equals(e.FileName, StringComparison.OrdinalIgnoreCase));

            if (zipEntry != null)
            {
                XElement containerXml;
                using (MemoryStream memoryStream = new MemoryStream())
                {
                    zipEntry.Extract(memoryStream);
                    memoryStream.Position = 0;

                    containerXml = XElement.Load(memoryStream);

                    XAttribute xmlns = containerXml.Attribute("xmlns");
                    XNamespace xNamespace = XNamespace.None;
                    if (xmlns != null)
                    {
                        xNamespace = xmlns.Value;
                    }
                    else
                    {
                        //get stream just in case we have a namespace prefix
                        memoryStream.Position = 0;
                        var sr = new System.IO.StreamReader(memoryStream);
                        string tmpXMLStream = sr.ReadToEnd();

                        //remove odfc namespace prefix and process
                        XDocument xDocument = XDocument.Parse(tmpXMLStream);
                        xDocument.Root.Add(new XAttribute("xmlns", "urn:oasis:names:tc:opendocument:xmlns:container"));
                        xDocument.Root.Attributes(XNamespace.Xmlns + "odfc").Remove();
                        containerXml = XElement.Parse(xDocument.ToString());

                        xNamespace = containerXml.Attribute("xmlns").Value;
                    }

                    var query = from rf in containerXml.Descendants(xNamespace + "rootfile")
                                where rf.Attribute("full-path") != null &&
                                      rf.Attribute("media-type") != null &&
                                      "application/oebps-package+xml".Equals(rf.Attribute("media-type").Value, StringComparison.OrdinalIgnoreCase)
                                select rf;

                    XElement xe = query.FirstOrDefault();

                    if (xe != null)
                    {
                        fullpath = xe.Attribute("full-path").Value;
                    }
                }
            }

            return fullpath;
        }

        private static Nav[] GetEpubNavs(Package package, ZipFile zipFile, string contentPath)
        {
            List<Nav> navs = new List<Nav>();
            if (package.Manifest.Any(x => "application/x-dtbncx+xml" == x.MediaType))
            {
                Item ncxItem = package.Manifest.First(x => "application/x-dtbncx+xml" == x.MediaType);
                string pathoffset = string.Empty;
                string[] hrefparts = ncxItem.Href.Split(new[] { "/" }, StringSplitOptions.RemoveEmptyEntries);
                if (hrefparts.Length > 1)
                {
                    pathoffset = string.Join("/", hrefparts.Take(hrefparts.Length - 1));
                }
                string fileName = string.Format("{0}/{1}", contentPath, ncxItem.Href);
                ZipEntry ncxEntry = zipFile.Entries.FirstOrDefault(x => x.FileName == fileName);

                if (ncxEntry != null)
                {
                    XElement ncxXml = null;
                    using (MemoryStream memoryStream = new MemoryStream())
                    {
                        ncxEntry.Extract(memoryStream);
                        memoryStream.Position = 0;

                        ncxXml = XElement.Load(memoryStream);
                    }

                    XNamespace nspace = ncxXml.Attribute("xmlns") == null ? ncxXml.Name.Namespace : ncxXml.Attribute("xmlns").Value;

                    // navMap
                    XElement navMap = ncxXml.Element(nspace + "navMap");

                    // navPoint
                    foreach (XElement np in navMap.Elements())
                    {
                        navs.Add(ConverNavPointToNav(np, nspace, pathoffset));
                    }
                }
            }

            return navs.ToArray();
        }

        private static Nav ConverNavPointToNav(XElement navPoint, XNamespace xNamespace, string pathoffset)
        {
            string id = string.Empty;
            string href = string.Empty;
            string title = string.Empty;
            int order = 0;
            List<Nav> children = new List<Nav>();

            // <navPoint id="toc4" playOrder="4">
            foreach (XAttribute attr in navPoint.Attributes())
            {
                switch (attr.Name.LocalName)
                {
                    case "id":
                        id = attr.Value;
                        break;
                    case "playOrder":
                        int.TryParse(attr.Value, out order);
                        break;
                }
            }

            foreach (XElement e in navPoint.Elements())
            {
                switch (e.Name.LocalName)
                {
                    case "navLabel":


                        // <text>title...</text>
                        XElement textAttr = e.Element(xNamespace + "text");
                        if (textAttr != null)
                        {
                            title = System.Net.HttpUtility.HtmlDecode(textAttr.Value);
                        }
                        break;

                    case "content":
                        // <content src="xhtml/Chapter01.html"/>
                        XAttribute srcAttr = e.Attribute("src");
                        if (srcAttr != null)
                        {
                            href = srcAttr.Value;
                        }
                        break;

                    case "navPoint":
                        // children
                        children.Add(ConverNavPointToNav(e, xNamespace, pathoffset));
                        break;
                }
            }

            if (string.IsNullOrEmpty(pathoffset) == false)
            {
                href = string.Format(@"{0}/{1}", pathoffset, href);
            }

            return new Nav(id, title, href, order, children.ToArray());
        }

        private static Nav[] GetEpub3Navs(Package package, ZipFile zipFile, string contentPath)
        {
            List<Nav> navs = new List<Nav>();
            if (package.Manifest.Any(x => x.Properties.Contains("nav") && "application/xhtml+xml" == x.MediaType))
            {
                Item navItem = package.Manifest.First(x => x.Properties.Contains("nav") && "application/xhtml+xml" == x.MediaType);
                string pathoffset = string.Empty;
                string[] hrefparts = navItem.Href.Split(new[] { "/" }, StringSplitOptions.RemoveEmptyEntries);
                if (hrefparts.Length > 1)
                {
                    pathoffset = string.Join("/", hrefparts.Take(hrefparts.Length - 1));
                }
                string fileName = string.Format("{0}/{1}", contentPath, navItem.Href);
                ZipEntry navEntry = zipFile.Entries.FirstOrDefault(x => x.FileName == fileName);

                if (navEntry != null)
                {
                    XElement navXml = null;
                    using (MemoryStream memoryStream = new MemoryStream())
                    {
                        navEntry.Extract(memoryStream);
                        memoryStream.Position = 0;

                        navXml = XElement.Load(memoryStream);
                    }

                    XNamespace nspace = navXml.Attribute("xmlns") == null ? XNamespace.None : navXml.Attribute("xmlns").Value;
                    XNamespace epubspace = navXml.Attribute(XNamespace.Xmlns + "epub") == null ? nspace : navXml.Attribute(XNamespace.Xmlns + "epub").Value;

                    // find <nav epub:type="toc" id="toc">
                    XElement navMap = navXml.Descendants(nspace + "nav")
                                        .FirstOrDefault(x =>
                                        x.Attribute(epubspace + "type") != null &&
                                        "toc".Equals(x.Attribute(epubspace + "type").Value, StringComparison.OrdinalIgnoreCase));
                    if (navMap != null)
                    {
                        XElement olXml = navMap.Element(nspace + "ol");
                        if (olXml != null)
                        {
                            // li
                            foreach (XElement li in olXml.Elements())
                            {
                                navs.Add(ConverLiToNav(li, nspace, pathoffset));
                            }
                        }
                    }
                }
            }

            return navs.ToArray();
        }

        private static Nav ConverLiToNav(XElement liElement, XNamespace xNamespace, string pathoffset)
        {
            string href = string.Empty;
            string title = string.Empty;
            string id = string.Empty;
            List<Nav> children = new List<Nav>();

            foreach (XAttribute attr in liElement.Attributes())
            {
                // <li id="id" ...
                switch (attr.Name.LocalName)
                {
                    case "id":
                        id = attr.Value;
                        break;
                }
            }

            foreach (XElement e in liElement.Elements())
            {
                switch (e.Name.LocalName)
                {
                    case "a":
                        // <a href=...>title</a>
                        title = System.Net.HttpUtility.HtmlDecode(e.Value);
                        XAttribute hrefAttr = e.Attribute("href");
                        if (hrefAttr != null)
                        {
                            href = hrefAttr.Value;
                        }
                        break;

                    case "ol":
                        // children
                        foreach (XElement child in e.Elements())
                        {
                            children.Add(ConverLiToNav(child, xNamespace, pathoffset));
                        }
                        break;
                }
            }

            if (string.IsNullOrEmpty(pathoffset) == false)
            {
                href = string.Format(@"{0}/{1}", pathoffset, href);
            }

            return new Nav(id, title, href, children.ToArray());
        }

        private byte[] GetBinaryResources(string source)
        {
            if (resources.ContainsKey(source) == false)
            {
                ZipEntry fileEntity = epubFile.Entries.FirstOrDefault(x => source.Equals(x.FileName, StringComparison.OrdinalIgnoreCase));
                if (fileEntity != null)
                {
                    using (MemoryStream ms = new MemoryStream())
                    {
                        fileEntity.Extract(ms);
                        ms.Position = 0;

                        byte[] buffer = new byte[ms.Length];
                        ms.Read(buffer, 0, buffer.Length);

                        resources.Add(source, buffer);
                    }
                }
                else
                {
                    resources.Add(source, new byte[0]);
                }
            }

            return resources[source];
        }

        private string CssEvaluator(Match match)
        {
            string href = match.Groups["href"].Value;
            string embed = string.Empty;
            if (Package.Manifest.Any(s => href.Contains(s.Href)))
            {
                Item mani = Package.Manifest.FirstOrDefault(s => href.Contains(s.Href));
                string source = string.Format("{0}/{1}", contentFolder, mani.Href);
                byte[] data = GetBinaryResources(source);
                string css = Encoding.UTF8.GetString(data, 0, data.Length);

                // remove body { ... }
                embed = string.Format("<style type='text/css'>{0}</style>", css);
            }

            return embed;
        }

        private string ImagesEvaluator(Match match)
        {
            string src = match.Groups["src"].Value;
            string embedimg = string.Empty;
            if (Package.Manifest.Any(s => src.Contains(s.Href)))
            {
                Item mani = Package.Manifest.FirstOrDefault(s => src.Contains(s.Href));
                byte[] data = GetBinaryResources(string.Format("{0}/{1}", contentFolder, mani.Href));
                string prefix = match.Groups["prefix"].Value;
                string suffix = match.Groups["suffix"].Value;

                embedimg = string.Format("{0}data:{1};base64,{2}{3}", prefix, mani.MediaType, Convert.ToBase64String(data), suffix);
            }

            return embedimg;
        }

        #endregion

        #region Implementation of System.IDisposable

        public void Dispose()
        {
            if (epubFile != null)
            {
                epubFile.Dispose();
                epubFile = null;
            }

            resources.Clear();
        }

        #endregion
    }
}
